package com.opdar.plugins.mybatis.core;

import com.opdar.plugins.mybatis.annotations.*;
import com.opdar.plugins.mybatis.annotations.Collection;
import com.opdar.plugins.mybatis.annotations.Date;
import com.opdar.plugins.mybatis.utils.Utils;
import com.sun.org.apache.xerces.internal.dom.DeferredTextImpl;
import com.sun.org.apache.xerces.internal.dom.TextImpl;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.*;

/**
 * Created by shiju on 2016/12/7.
 */
public class ResultMapperParser {
    private String namespace = "";
    private HashSet<String> ids = new HashSet<String>();
    private HashSet<String> resultMaps = new HashSet<String>();
    private String prefix;
    private String databaseName;
    private static List<String> namespaces = new ArrayList<String>();

    public void init(String prefix, String databaseName) throws ParserConfigurationException, SAXException, IOException, ClassNotFoundException {
        this.prefix = prefix;
        this.databaseName = databaseName;
    }

    public ByteArrayInputStream parse(InputStream readerXml) throws ParserConfigurationException, IOException, SAXException, ClassNotFoundException {
        DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
        dBuilder.setEntityResolver(new EntityResolver() {

            @Override
            public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
                return new InputSource(new ByteArrayInputStream(new byte[0]));
            }
        });
        Document doc = dBuilder.parse(readerXml);

        //解析Mapper
        parseMapper(doc);
        if (namespaces.contains(namespace)) {
            return null;
        } else {
            namespaces.add(namespace);
        }

        //新写一个mapper
        doc.getDocumentElement().normalize();
        Element mapper = (Element) doc.getElementsByTagName("mapper").item(0);
        Element resutMap = createResultMapByNamespace(doc, namespace);
        Element selectList = createSelectListByNamespace(doc);
        Element selectOne = createSelectOneByNamespace(doc);
        Element count = createCountByNamespace(doc);
        Element update = createUpdateByNamespace(doc);
        Element insert = createInsertByNamespace(doc);
        Element delete = createDeleteByNamespace(doc);

        if (resutMap != null) {
            mapper.insertBefore(resutMap, mapper.getFirstChild());
        }
        if (!ids.contains("selectList")) {
            mapper.appendChild(selectList);
        }
        if (!ids.contains("selectOne")) {
            mapper.appendChild(selectOne);
        }
        if (!ids.contains("count")) {
            mapper.appendChild(count);
        }
        if (!ids.contains("insert")) {
            mapper.appendChild(insert);
        }
        if (!ids.contains("update")) {
            mapper.appendChild(update);
        }
        if (!ids.contains("delete")) {
            mapper.appendChild(delete);
        }
        TransformerFactory transFactory = TransformerFactory.newInstance();
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            Transformer transformer = transFactory.newTransformer();
            transformer.setOutputProperty("encoding", doc.getXmlEncoding());
            transformer.setOutputProperty("indent", "yes");
            transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, doc.getDoctype().getPublicId());
            transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, doc.getDoctype().getSystemId());
            DOMSource source = new DOMSource();
            source.setNode(doc);
            doc.setXmlStandalone(true);
            StreamResult result = new StreamResult();
            result.setOutputStream(baos);
            transformer.transform(source, result);
            return new ByteArrayInputStream(baos.toByteArray());
        } catch (TransformerConfigurationException e) {
            e.printStackTrace();
        } catch (TransformerException e) {
            e.printStackTrace();
        }
        return null;
    }

    private void parseMapper(Document doc) {
        Element mapper = (Element) doc.getElementsByTagName("mapper").item(0);
        namespace = mapper.getAttribute("namespace");
        NodeList nodes = mapper.getChildNodes();
        for (int i = 0; i < nodes.getLength(); i++) {
            Node node = nodes.item(i);
            if (node instanceof Element) {
                String qName = node.getNodeName();
                if (qName.equals("resultMap")) {
                    String id = ((Element) node).getAttribute("id");
                    resultMaps.add(id);
                }
                if (qName.equals("select") || qName.equals("insert") || qName.equals("update") || qName.equals("delete")) {
                    String id = ((Element) node).getAttribute("id");
                    ids.add(id);
                }
                for (int j = 0; j < node.getChildNodes().getLength(); j++) {
                    Node node2 = node.getChildNodes().item(j);
                    if (node2 instanceof DeferredTextImpl ) {
                        String text = ((DeferredTextImpl) node2).getWholeText();
                        text = text.replaceAll("`database`.`t_", "`" + databaseName + "`.`" + prefix + "");
                        ((DeferredTextImpl ) node2).replaceWholeText(text);
                    }
                }
            }
//
        }
    }

    private Element createDeleteByNamespace(Document doc) throws ClassNotFoundException {
        Element select = doc.createElement("delete");
        select.setAttribute("id", "delete");
        Class<?> entityClz = classForNamespace();
        select.setAttribute("parameterType", entityClz.getName());
        String tableName = Utils.testUnderscore(Utils.replaceSuffix(entityClz.getSimpleName()), false);
        Namespace namespace = entityClz.getAnnotation(Namespace.class);
        if (namespace != null) {
            if (namespace.tableName().length() > 0) {
                tableName = namespace.tableName();
            }
        }
        //语句
        select.appendChild(doc.createTextNode("DELETE FROM `" + databaseName + "`.`" + prefix + tableName + "` "));
        Element where = doc.createElement("trim");
        where.setAttribute("prefix", "WHERE");
        where.setAttribute("prefixOverrides", "AND |OR");
        Field[] fields = entityClz.getDeclaredFields();
        StringBuilder builder = new StringBuilder();
        StringBuilder as = new StringBuilder();
        select(doc, where, fields, builder, as);

        if (builder.length() > 0) {
            builder.deleteCharAt(builder.length() - 1);
        }
        select.appendChild(where);
        Node orderBy = doc.createTextNode(builder.toString());
        select.appendChild(orderBy);
        return select;
    }

    private Element createInsertByNamespace(Document doc) throws ClassNotFoundException {
        Element select = doc.createElement("insert");
        select.setAttribute("id", "insert");
        Class<?> entityClz = classForNamespace();
        select.setAttribute("parameterType", entityClz.getName());
        String tableName = Utils.testUnderscore(Utils.replaceSuffix(entityClz.getSimpleName()), false);
        Namespace namespace = entityClz.getAnnotation(Namespace.class);
        if (namespace != null) {
            if (namespace.tableName().length() > 0) {
                tableName = namespace.tableName();
            }
        }
        //语句
        select.appendChild(doc.createTextNode("insert into `" + databaseName + "`.`" + prefix + tableName + "` "));
        Field[] fields = entityClz.getDeclaredFields();

        Element fieldElement = doc.createElement("trim");
        fieldElement.setAttribute("prefix", "(");
        fieldElement.setAttribute("suffix", ")");
        fieldElement.setAttribute("prefixOverrides", ",");
        //where
        Element set = doc.createElement("trim");
        set.setAttribute("prefix", "(");
        set.setAttribute("suffix", ")");
        set.setAttribute("prefixOverrides", ",");

        for (Field field : fields) {
            field.setAccessible(true);
            Association association = field.getAnnotation(Association.class);
            Collection collection = field.getAnnotation(Collection.class);
            if (association != null || collection !=null) continue;
            com.opdar.plugins.mybatis.annotations.Field field1 = field.getAnnotation(com.opdar.plugins.mybatis.annotations.Field.class);
            if (field1 != null && field1.insert() || field1 == null) {
                String name = field.getName();
                String scname = Utils.testUnderscore(name, false);
                Element ifElement = doc.createElement("if");
                ifElement.setAttribute("test", name + " != null");
                ifElement.setTextContent(",`" + scname + "`");
                Element ifElement2 = doc.createElement("if");
                ifElement2.setAttribute("test", name + " != null");
                ifElement2.setTextContent(",#{" + name + "}");
                fieldElement.appendChild(ifElement);
                set.appendChild(ifElement2);
            }

        }

        select.appendChild(fieldElement);
        select.appendChild(doc.createTextNode(" values "));
        select.appendChild(set);
        return select;
    }

    private Element createUpdateByNamespace(Document doc) throws ClassNotFoundException {
        Element select = doc.createElement("update");
        select.setAttribute("id", "update");
        Class<?> entityClz = classForNamespace();
        select.setAttribute("parameterType", entityClz.getName());
        String tableName = Utils.testUnderscore(Utils.replaceSuffix(entityClz.getSimpleName()), false);
        Namespace namespace = entityClz.getAnnotation(Namespace.class);
        if (namespace != null) {
            if (namespace.tableName().length() > 0) {
                tableName = namespace.tableName();
            }
        }
        //语句
        select.appendChild(doc.createTextNode("update `" + databaseName + "`.`" + prefix + tableName + "` "));
        Field[] fields = entityClz.getDeclaredFields();

        //where
        Element where = doc.createElement("trim");
        where.setAttribute("prefix", "WHERE");
        where.setAttribute("prefixOverrides", "AND |OR");

        Element set = doc.createElement("trim");
        set.setAttribute("prefix", "set");
        set.setAttribute("prefixOverrides", ",");

        HashMap<String, List<Element>> ors = new HashMap<String, List<Element>>();
        for (Field field : fields) {
            field.setAccessible(true);
            Association association = field.getAnnotation(Association.class);
            Collection collection = field.getAnnotation(Collection.class);
            if (association != null || collection !=null) continue;
            String name = field.getName();
            String scname = Utils.testUnderscore(name, false);
            Element ifElement = doc.createElement("if");
            ifElement.setAttribute("test", "where." + name + " != null");
            Like like = field.getAnnotation(Like.class);
            OR or = field.getAnnotation(OR.class);
            Date date = field.getAnnotation(Date.class);

            String extra1 = "", extra2 = "";
            if (date != null) {
                extra1 = " DATE(";
                extra2 = ") ";
            }

            com.opdar.plugins.mybatis.annotations.Field field1 = field.getAnnotation(com.opdar.plugins.mybatis.annotations.Field.class);

            Where where1 = field.getAnnotation(Where.class);
            if (where1 != null && where1.plain()) {
                where.appendChild(doc.createTextNode(where1.value()));
            } else if (where1 != null) {
                Element whereIfElement = doc.createElement("if");
                whereIfElement.setAttribute("test", "where." + name + " != null");
                whereIfElement.setTextContent(" " + where1.value() + "#{where." + name + "}");
                where.appendChild(whereIfElement);
            }

            if (field1 != null && field1.update() && field1.where() || field1 == null) {

                String prefix = "AND";
                List<Element> group = null;
                if (or != null) {
                    String groupName = or.value();
                    if (!ors.containsKey(groupName)) {
                        ors.put(groupName, group = new LinkedList<Element>());
                    } else {
                        group = ors.get(groupName);
                    }
                    prefix = "OR";
                }
                if (like != null) {
                    ifElement.setTextContent(prefix + extra1 + " `" + scname + "`" + extra2 + " like " + extra1 + "'" + like.prefix() + "${where." + name + "}" + like.suffix() + "'" + extra2);
                } else {
                    ifElement.setTextContent(prefix + " `" + scname + "`" + " = #{where." + name + "}");
                }
                if (or != null) {
                    group.add(ifElement);
                } else {
                    where.appendChild(ifElement);
                }
            }

            if (field1 != null && field1.update() || field1 == null) {
                Element setifElement = doc.createElement("if");
                setifElement.setAttribute("test", "update." + name + " != null");
                if (field1 != null && field1.plain()) {
                    setifElement.setTextContent(", `" + scname + "`" + " = ${update." + name + "}");
                } else {
                    setifElement.setTextContent(", " + extra1 + "`" + scname + "`" + extra2 + " = " + extra1 + "#{update." + name + "}" + extra2);
                }
                set.appendChild(setifElement);
            }
        }

        appendOR(doc, where, ors);
        select.appendChild(set);
        select.appendChild(where);
        return select;
    }

    private Element createCountByNamespace(Document doc) throws ClassNotFoundException {
        Element select = doc.createElement("select");
        select.setAttribute("id", "count");
        Class<?> entityClz = classForNamespace();
        select.setAttribute("parameterType", entityClz.getName());
        String resultMapName = getResultMapName(entityClz);
        select.setAttribute("resultType", "int");
        String tableName = Utils.testUnderscore(Utils.replaceSuffix(entityClz.getSimpleName()), false);
        Namespace namespace = entityClz.getAnnotation(Namespace.class);
        if (namespace != null) {
            if (namespace.tableName().length() > 0) {
                tableName = namespace.tableName();
            }
        }
        //语句
        select.appendChild(doc.createTextNode("SELECT count(*) FROM `" + databaseName + "`.`" + prefix + tableName + "` "));
        Element where = doc.createElement("trim");
        where.setAttribute("prefix", "WHERE");
        where.setAttribute("prefixOverrides", "AND |OR");

        Field[] fields = entityClz.getDeclaredFields();
        StringBuilder builder = new StringBuilder();
        StringBuilder as = new StringBuilder();
        select(doc, where, fields, builder, as);

        if (builder.length() > 0) {
            builder.deleteCharAt(builder.length() - 1);
        }
        select.appendChild(where);
        Node orderBy = doc.createTextNode(builder.toString());
        select.appendChild(orderBy);
        return select;
    }

    private Element createSelectOneByNamespace(Document doc) throws ClassNotFoundException {
        Element select = doc.createElement("select");
        select.setAttribute("id", "selectOne");
        Class<?> entityClz = classForNamespace();
        select.setAttribute("parameterType", entityClz.getName());
        String resultMapName = getResultMapName(entityClz);
        select.setAttribute("resultMap", resultMapName);
        String tableName = Utils.testUnderscore(Utils.replaceSuffix(entityClz.getSimpleName()), false);
        Namespace namespace = entityClz.getAnnotation(Namespace.class);
        if (namespace != null) {
            if (namespace.tableName().length() > 0) {
                tableName = namespace.tableName();
            }
        }
        //语句
        Element where = doc.createElement("trim");
        where.setAttribute("prefix", "WHERE");
        where.setAttribute("prefixOverrides", "AND |OR");

        Field[] fields = entityClz.getDeclaredFields();
        StringBuilder builder = new StringBuilder();
        StringBuilder as = new StringBuilder();
        select(doc, where, fields, builder, as);

        if (builder.length() > 0) {
            builder.deleteCharAt(builder.length() - 1);
        }
        builder.append(" limit 1");
        select.appendChild(doc.createTextNode("SELECT *" + as.toString() + " FROM `" + databaseName + "`.`" + prefix + tableName + "` "));
        select.appendChild(where);
        Node orderBy = doc.createTextNode(builder.toString());
        select.appendChild(orderBy);
        return select;
    }

    private void select(Document doc, Element where, Field[] fields, StringBuilder builder, StringBuilder asBuilder) {
        this.select(doc, where, fields, builder, asBuilder, false);
    }

    private void select(Document doc, Element where, Field[] fields, StringBuilder builder, StringBuilder asBuilder, boolean isDelete) {
        HashMap<String, List<Element>> ors = new HashMap<String, List<Element>>();
        for (Field field : fields) {
            field.setAccessible(true);
            Association association = field.getAnnotation(Association.class);
            Collection collection = field.getAnnotation(Collection.class);
            if (association != null || collection !=null) continue;
            com.opdar.plugins.mybatis.annotations.Field field1 = field.getAnnotation(com.opdar.plugins.mybatis.annotations.Field.class);
            if (field1 != null && (isDelete ? field1.delete() : field1.select()) || field1 == null) {
                String name = field.getName();
                String scname = Utils.testUnderscore(name, false);
                Element ifElement = doc.createElement("if");
                ifElement.setAttribute("test", name + " != null");
                Like like = field.getAnnotation(Like.class);
                Not not = field.getAnnotation(Not.class);
                OR or = field.getAnnotation(OR.class);
                Date date = field.getAnnotation(Date.class);
                As as = field.getAnnotation(As.class);
                if (as != null) {
                    asBuilder.append(",").append(as.value()).append(" as ").append(as.name());
                }

                String extra1 = "", extra2 = "";
                if (date != null) {
                    extra1 = " DATE(";
                    extra2 = ") ";
                }

                Where where1 = field.getAnnotation(Where.class);
                if (where1 != null && where1.plain()) {
                    where.appendChild(doc.createTextNode(where1.value()));
                } else if (where1 != null) {
                    Element whereIfElement = doc.createElement("if");
                    whereIfElement.setAttribute("test", name + " != null");
                    whereIfElement.setTextContent(" " + where1.value() + "#{" + name + "}");
                    where.appendChild(whereIfElement);
                }
                if (field1 != null && field1.where() || field1 == null) {
                    if (not != null) {
                        Element notIfElement = doc.createElement("if");
                        notIfElement.setAttribute("test", name + " == null");
                        notIfElement.setTextContent(" AND `" + scname + "`" + " <> '" + not.value() + "'");
                        where.appendChild(notIfElement);
                    }
                    String prefix = "AND";
                    List<Element> group = null;
                    if (or != null) {
                        String groupName = or.value();
                        if (!ors.containsKey(groupName)) {
                            ors.put(groupName, group = new LinkedList<Element>());
                        } else {
                            group = ors.get(groupName);
                        }
                        prefix = "OR";
                    }
                    if (like != null) {
                        ifElement.setTextContent(prefix + extra1 + " `" + scname + "`" + extra2 + " like " + extra1 + "'" + like.prefix() + "${" + name + "}" + like.suffix() + "'" + extra2);
                    } else {
                        ifElement.setTextContent(prefix + extra1 + " `" + scname + "`" + extra2 + " = " + extra1 + "#{" + name + "}" + extra2);
                    }
                    if (or != null) {
                        group.add(ifElement);
                    } else {
                        where.appendChild(ifElement);
                    }
                }
                //排序字段
                Sort sort = field.getAnnotation(Sort.class);
                if (sort != null) {
                    if (builder.length() == 0) {
                        builder.append("order by ");
                    }
                    builder.append(scname).append(" ").append(sort.type().name());
                    builder.append(",");
                }
            }
        }
        appendOR(doc, where, ors);
    }

    private void appendOR(Document doc, Element where, HashMap<String, List<Element>> ors) {
        for (String key : ors.keySet()) {
            List<Element> list = ors.get(key);

            Element trim = doc.createElement("trim");
            trim.setAttribute("prefix", "AND (");
            trim.setAttribute("suffix", ")");
            trim.setAttribute("prefixOverrides", "AND |OR");
            for (Element element : list) {
                trim.appendChild(element);
            }
            where.appendChild(trim);
        }
    }

    private Element createSelectListByNamespace(Document doc) throws ClassNotFoundException {
        Element select = doc.createElement("select");
        select.setAttribute("id", "selectList");
        Class<?> entityClz = classForNamespace();
        select.setAttribute("parameterType", entityClz.getName());
        String resultMapName = getResultMapName(entityClz);
        select.setAttribute("resultMap", resultMapName);
        String tableName = Utils.testUnderscore(Utils.replaceSuffix(entityClz.getSimpleName()), false);
        Namespace namespace = entityClz.getAnnotation(Namespace.class);
        if (namespace != null) {
            if (namespace.tableName().length() > 0) {
                tableName = namespace.tableName();
            }
        }
        //语句
        Element where = doc.createElement("trim");
        where.setAttribute("prefix", "WHERE");
        where.setAttribute("prefixOverrides", "AND |OR");

        Field[] fields = entityClz.getDeclaredFields();
        StringBuilder builder = new StringBuilder();
        StringBuilder as = new StringBuilder();
        select(doc, where, fields, builder, as);

        if (builder.length() > 0) {
            builder.deleteCharAt(builder.length() - 1);
        }
        select.appendChild(doc.createTextNode("SELECT *" + as.toString() + " FROM `" + databaseName + "`.`" + prefix + tableName + "` "));
        select.appendChild(where);
        Node orderBy = doc.createTextNode(builder.toString());
        select.appendChild(orderBy);
        return select;
    }

    public Class<?> classForNamespace() throws ClassNotFoundException {
        return classForNamespace(namespace);
    }

    public Class<?> classForNamespace(String namespace) throws ClassNotFoundException {
        Class<?> clz = Class.forName(namespace);
        Type[] interfaces = clz.getGenericInterfaces();
        if (interfaces.length > 0 && interfaces[0] instanceof ParameterizedTypeImpl) {
            return (Class<?>) ((ParameterizedTypeImpl) interfaces[0]).getActualTypeArguments()[0];
        }
        return null;
    }

    /**
     * 通过命名空间创建ResultMap
     *
     * @param doc
     * @param namespace
     * @return
     * @throws ClassNotFoundException
     */
    private Element createResultMapByNamespace(Document doc, String namespace) throws ClassNotFoundException {
        Element resutMap = doc.createElement("resultMap");
        Class<?> entityClz = classForNamespace();
        if (entityClz != null) {
            String resultMapName = getResultMapName(entityClz);
            if (resultMaps.contains(resultMapName)) return null;
            resutMap.setAttribute("id", resultMapName);
            resutMap.setAttribute("type", entityClz.getName());
            Field[] fields = entityClz.getDeclaredFields();
            Map<String, String> ids = new HashMap<String, String>();

            List<Element> associations = new LinkedList<Element>();
            List<Element> collections = new LinkedList<Element>();
            for (Field field : fields) {
                field.setAccessible(true);
                Association association = field.getAnnotation(Association.class);
                Collection collection = field.getAnnotation(Collection.class);
                Id id = field.getAnnotation(Id.class);
                String name = field.getName();
                String scname = Utils.testUnderscore(name, false);
                Element element = null;
                if (association != null) {
                    element = doc.createElement("association");
                    element.setAttribute("column", ids.toString());
                    element.setAttribute("javaType", field.getType().getName());
                    element.setAttribute("select", association.mapper().getName().concat(".").concat(association.select()));
                    element.setAttribute("property", name);
                    associations.add(element);
                } else if(collection != null){
                    element = doc.createElement("collection");
                    element.setAttribute("property", name);
                    element.setAttribute("column", ids.toString());
                    element.setAttribute("select", collection.mapper().getName().concat(".").concat(collection.select()));

                    Type type = field.getGenericType();
                    if(type instanceof ParameterizedTypeImpl){
                        Class<?> rawType = ((ParameterizedTypeImpl) type).getRawType();
                        if(java.util.Collection.class.isAssignableFrom(rawType)){
                            //this is collection
                            Class<?> actualType = (Class<?>) ((ParameterizedTypeImpl) type).getActualTypeArguments()[0];
                            element.setAttribute("javaType", rawType.getName());
                            element.setAttribute("ofType", actualType.getName());
                            collections.add(element);
                        }
                    }
                }else{
                    if (id != null) {
                        ids.put(Utils.replaceSuffix(entityClz.getSimpleName()).toLowerCase() + Utils.testField(name), scname);
                        element = doc.createElement("id");
                    } else {
                        element = doc.createElement("result");
                    }
                    element.setAttribute("column", scname);
                    element.setAttribute("property", name);
                    resutMap.appendChild(element);
                }
            }
            for (Element e : associations) {
                resutMap.appendChild(e);
            }
            for (Element e : collections) {
                resutMap.appendChild(e);
            }
        }
        return resutMap;
    }

    private String getResultMapName(Class<?> entityClz) {
        String resultMapName = Utils.replaceSuffix(entityClz.getSimpleName()).concat("Result");
        resultMapName = Character.toLowerCase(resultMapName.charAt(0)) + resultMapName.substring(1);
        return resultMapName;
    }

}
